import java.util.Date;
import java.util.concurrent.*;

/**
 * Connection pool for concurrently accessing to some source.
 * @param <T> Type of source.
 */
public class ConnectionPoolImplementation<T> implements ConnectionPool<T> {
    private long keepAlive;
    private TimeUnit unit;
    private int min;

    private Semaphore semaphore;

    private ConnectionSource<T> source;
    private ConcurrentLinkedDeque<ConnectionSourceWithDelay> connections;

    /**
     * Inner class for resource wrap. Store connection to source and
     * time of last usage.
     */
    private class ConnectionSourceWithDelay {
        private T connection;
        private Date timestamp;

        public ConnectionSourceWithDelay(T connection) {
            this.connection = connection;
            this.timestamp = new Date();
        }

        public T getConnection() {
            return connection;
        }

        public Date getTimestamp() {
            return timestamp;
        }
    }

    public ConnectionPoolImplementation(ConnectionSource<T> source,
                                        int min, int max,
                                        long keepAlive, TimeUnit unit) {
        initializeStates(source, min, max, keepAlive, unit);
        createConnectionsToSource(min);
        createThreadForKeepAlive();
    }

    private void initializeStates(ConnectionSource<T> source,
                                  int min, int max,
                                  long keepAlive, TimeUnit unit) {
        this.source = source;
        this.min = min;
        this.keepAlive = keepAlive;
        this.unit = unit;
        this.semaphore = new Semaphore(max, true);
        this.connections = new ConcurrentLinkedDeque<>();
    }

    /**
     * Create fix connections to source.
     * At first there are min(capacity, i.e. capacity ==min == max) count of connections.
     * @param count Count of creations.
     */
    private void createConnectionsToSource(int count) {
        for (int i = 0; i < count; i++) {
            this.connections.addFirst(new ConnectionSourceWithDelay(this.source.open()));
        }
    }

    /**
     * Thread for cleaning connection pool from old connections.
     * Each keep alive time the thread is looking for connections that were
     * last used more the keepAliveTime time ago.
     */
    private void createThreadForKeepAlive() {
        if (keepAlive == 0) {
            return;
        }
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleAtFixedRate(() -> {
            if (connections.size() > min) {
                Date now = new Date();
                try {
                    for (ConnectionSourceWithDelay element : connections) {
                        Date timestamp = element.getTimestamp();
                        if (now.getTime()- timestamp.getTime() >
                                TimeUnit.MILLISECONDS.convert(keepAlive, unit) &&
                                connections.size() > min) {
                            connections.remove(element);
                        }
                    }
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }
        }, 0, keepAlive, unit);
    }

    /**
     * Getting connection from connection pool.
     * I.e connections in pool are sorted by timestamp, method return the last
     * usage connection.
     * @return Last usage connection from pool.
     */
    private T getSource() {
        if (connections.isEmpty()) {
            createConnectionsToSource(1);
        }
        return connections.removeLast().getConnection();
    }

    @Override
    public T get() throws InterruptedException {
        semaphore.acquire();
        return getSource();
    }

    @Override
    public T tryGet() {
        if (semaphore.tryAcquire()) {
            return getSource();
        }
        return null;
    }

    @Override
    public T tryGet(long time, TimeUnit unit) throws InterruptedException {
        if (semaphore.tryAcquire(time, unit)) {
            return getSource();
        }
        return null;
    }

    @Override
    public void release(T object) {
        this.connections.add(new ConnectionSourceWithDelay(object));
        semaphore.release();
    }

    @Override
    protected void finalize() throws Throwable {
        for (ConnectionSourceWithDelay elem : connections) {
            source.close(elem.getConnection());
        }
        connections.clear();
    }
}
